Functions as First-Class citizens

In functional programming, functions can be treated as objects. That is, they can assigned to a variable, can be passed as arguments or even returned from other functions.


In [2]:
a = 10
def test_function():
    pass
print(id(a), dir(a))
print(id(test_function), dir(test_function))


1798467328 ['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
2309665771728 ['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

The lambda

The simplest way to initialize a pure function in python is by using lambda keyword, which helps in defining the one-line function. Functions initialized with lambda can often called anonymous functions


In [2]:
# Example lambda keyword
product_func = lambda x, y: x*y

print(product_func(10, 20))
print(product_func(10, 2))


200
20

In [3]:
concat = lambda x, y: [x, y]

print(concat([1,2,3], 4))


[[1, 2, 3], 4]

Functions as Objects

Functions are first-class objects in Python, meaning they have attributes and can be referenced and assigned to variables.


In [4]:
def square(x):
    """
    This returns the square of the requested number `x`
    """
    return x**2

print(square(10))


100

In [5]:
print(square(100))


10000

In [6]:
# Assignation to another variable
mySquare = square
print(mySquare(100))
print(square)
print(mySquare)
print(id(square))
print(id(mySquare))


10000
<function square at 0x7f30581b60d0>
<function square at 0x7f30581b60d0>
139845613347024
139845613347024

In [7]:
# attributes present
print("*"*30)
print(dir(square))
print("*"*30)
print(mySquare.__name__)
print("*"*30)
print(square.__code__)
print("*"*30)
print(square.__doc__)


******************************
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
******************************
square
******************************
<code object square at 0x7f3058216540, file "<ipython-input-4-37fb4737804d>", line 1>
******************************

    This returns the square of the requested number `x`
    

Adding attributes to a function


In [8]:
square.d = 10
print(dir(square))


['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'd']

higher-order function

Python also supports higher-order functions, meaning that functions can accept other functions as arguments and return functions to the caller.


In [3]:
print(square(square(square(2))))


256

In [12]:
product_func = lambda x, y: x*y

sum_func = lambda F, m: lambda x, y: F(x, y)+m

print(sum_func(product_func, 5)(2, 4))


13

In [13]:
print(sum_func)


<function <lambda> at 0x7f1b2c089cf8>

In [16]:
print(sum_func(product_func, 5))


<function <lambda> at 0x7f1b2c089f50>

In [15]:
print(sum_func(product_func, 5)(3, 5))


20

13=2*4+5 F -> product_func m => 5 x -> 2 y -> 4 2*4+5 = 8+5 = 13

In the above example higher-order function that takes two inputs- A function F(x) and a multiplier m.

Nested Functions

In Python, Function(s) can also be defined within the scope of another function. If this type of function definition is used the inner function is only in scope inside the outer function, so it is most often useful when the inner function is being returned (moving it to the outer scope) or when it is being passed into another function.

Notice that in the below example, a new instance of the function inner() is created on each call to outer(). That is because it is defined during the execution of outer(). The creation of the second instance has no impact on the first.


In [9]:
def outer(a):
    """
    Outer function 
    """
    y = 0
    
    def inner(x):
        """
        inner function
        """
        y = x*x*a 
        return(y)
    print(a)
    
    return inner

my_out = outer

In [10]:
my_out(102)


102
Out[10]:
<function __main__.outer.<locals>.inner>

In [12]:
o = outer(10)
b = outer(20)
print("*"*20)
print(b)
print(o)
print("*"*20)
print(o(10))
print(b(10))


10
20
********************
<function outer.<locals>.inner at 0x7f3058125730>
<function outer.<locals>.inner at 0x7f3058125598>
********************
1000
2000

In [13]:
def outer():
    """
    Outer function 
    """
    if 'a' in locals():
        a +=10
    else:
        print("~"),
        a = 20
    def inner(x):
        """
        inner function
        """
        return(x*x*a)
    print(a)
    return inner

# oo = outer
# print(oo.__doc__)
o = outer()
print("*"*20)
print(o)
print(o(10))
print(o.__doc__)


~
20
********************
<function outer.<locals>.inner at 0x7f3058125950>
2000

        inner function
        

In [14]:
b = outer()
print(b)
print(b(30))
print(b.__doc__)


~
20
<function outer.<locals>.inner at 0x7f3058125b70>
18000

        inner function
        

In [15]:
x = 0

def outer():
    x = 1
    def inner():
        x = 2
        print("inner:", x)

    inner()
    print("outer:", x)

outer()
print("global:", x)


inner: 2
outer: 1
global: 0

Problem with local and global

lets take the above example, we have two functions, outer & inner. We also have x variable which is present as global and also present in both the functions.

If we want to access x of outer function from inner function than global keyword not help. Fortunately, Python provides a keyword nonlocal which allows inner functions to access variables to outer functions as shown in below example.

The details of nonlocal are details in https://www.python.org/dev/peps/pep-3104/


In [14]:
x = 0
def outer():
    x = 1
    def inner():
        nonlocal x
        x = 2
        print("inner:",x, "id:", id(x))

    inner()
    print("outer:",x, "id:", id(x))

outer()
print("global:",x, "id:", id(x))


inner: 2 id: 140130654173632
outer: 2 id: 140130654173632
global: 0 id: 140130654173568

In [34]:
def outer(a):
    """
    Outer function 
    """
    y = 1
    def inner(x):
        """
        inner function
        """
        nonlocal y
        print(y)
        y = x*x*a 
        return("y =" + str(y))
    print(a)
    return inner

In [38]:
o = outer(10)
b = outer(20)
print("*"*20)
print(o)
print(o(10))
print("*"*20)
print(b)
print(b(10))


10
20
********************
<function outer.<locals>.inner at 0x7f3058130378>
1
y =1000
********************
<function outer.<locals>.inner at 0x7f305811ff28>
1
y =2000

Inner / Nested Functions - When to use

Encapsulation

You use inner functions to protect them from anything happening outside of the function, meaning that they are hidden from the global scope.


In [3]:
# Encapsulation

def increment(current):
    def inner_increment(x):  # hidden from outer code
        return x + 1
    next_number = inner_increment(current)
    return [current, next_number]

print(increment(10))


[10, 11]

NOTE: We can not access directly the inner function as shown below


In [4]:
try:
    increment.inner_increment(109)
except Exception  as e:
    print(e)


'function' object has no attribute 'inner_increment'

Following DRY (Don't Repeat Yourself)

This type can be used if you have a section of code base in function is repeated in numerous places. For example, you might write a function which processes a file, and you want to accept either an open file object or a file name:


In [20]:
# Keepin’ it DRY

def process(file_name):
    def do_stuff(file_process):
        for line in file_process:
            print(line)

    if isinstance(file_name, str):
        with open(file_name, 'r') as f:
            do_stuff(f)
    else:
        do_stuff(file_name)
        
process(["test", "test3", "t33"])
# process("test.txt")


test
test3
t33

or have similar logic which can be replaced by a function, such as mathematical functions, or code base which can be clubed by using some parameters.


In [21]:
def square(n):
    return n**2

def cube(n):
    return n**3

print(square(2))


4

In [22]:
def sqr(a, b):
    return a**b

??? why code


In [32]:
def test():
    print("TEST TEST TEST")
    
    def yes(name):
        print("Ja, ", name)
        return True
    return yes

d = test()
print("*" * 14)
a = d("Murthy")
print("*" * 14)
print(a)


TEST TEST TEST
**************
Ja,  Venky
**************
True

In [38]:
def power(exp):
    def subfunc(a):
        return a**exp
    return subfunc

square = power(2)
hexa = power(6)

print(square)
print(hexa)

print(square(5)) # 5**2
print()
print(hexa(3)) # 3**6
print(power(6)(3))
# subfunc(3) where exp = 6
# SQuare 

# exp -> 2 
# Square(5) 
# a -> 5 
# 5**2
# 25


<function power.<locals>.subfunc at 0x00000219C2D6BB70>
<function power.<locals>.subfunc at 0x00000219C2D6BEA0>
25

729
729

In [ ]:
Power(6)(3, x)

In [48]:
def a1(m):
    x = m * 2
    def b(v, t=None):
        if t:
            print(x, m, t)
            return v + t
        else:
            print(x, m, v)
            return v + x
    return b
n = a1(2)
print(n(3))
print(n(3, 10))


4 2 3
7
4 2 10
13

In [52]:
def f1(a):
    def f2(b):
        return f2
        def f3(c):
            return f3
            def f4(d):
                return f4
                def f5(e):
                    return f5
print (f1(1)(2)(3)(4)(5))


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-52-8b5968dce991> in <module>()
      8                 def f5(e):
      9                     return f5
---> 10 print (f1(1)(2)(3)(4)(5))

TypeError: 'NoneType' object is not callable

In [55]:
def f1(a):
    def f2(b):
        def f3(c):
            def f4(d):
                def f5(e):
                    print(e)
                return f5
            return f4
        return f3
    return f2
        
f1(1)(2)(3)(4)(5)


5

Closures & Factory Functions 1

They are techniques for implementing lexically scoped name binding with first-class functions. It is a record, storing a function together with an environment. a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.

A closure—unlike a plain function—allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.


In [12]:
def f(x):
    def g(y):
        return x + y
    return g

def h(x):
    return lambda y: x + y

a = f(1)
b = h(1)
print(a, b)
print(a(5), b(5))
print(f(1)(5), h(1)(5))


<function f.<locals>.g at 0x000001ECFA3A3268> <function h.<locals>.<lambda> at 0x000001ECFA3A3598>
6 6
6 6

both a and b are closures—or rather, variables with a closure as value—in both cases produced by returning a nested function with a free variable from an enclosing function, so that the free variable binds to the parameter x of the enclosing function. However, in the first case the nested function has a name, g, while in the second case the nested function is anonymous. The closures need not be assigned to a variable, and can be used directly, as in the last lines—the original name (if any) used in defining them is irrelevant. This usage may be deemed an "anonymous closure".

1: Copied from : "https://en.wikipedia.org/wiki/Closure_(computer_programming)"


In [25]:
def make_adder(x):
    def add(y):
        return x + y
    return add

plus10 = make_adder(10)
print(plus10(12))  # make_adder(10).add(12)
print(make_adder(10)(12))


22
22

Closures can avoid the use of global values and provides some form of data hiding. It can also provide an object oriented solution to the problem.

When there are few methods (one method in most cases) to be implemented in a class, closures can provide an alternate and more elegant solutions. But when the number of attributes and methods get larger, better implement a class.

In functional programming, functions can be treated as objects. That is, they can assigned to a variable, can be passed as arguments or even returned from other functions.


In [2]:
a = 10
def test_function():
    pass
print(id(a), dir(a))
print(id(test_function), dir(test_function))


1798467328 ['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
2309665771728 ['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

The lambda

The simplest way to initialize a pure function in python is by using lambda keyword, which helps in defining the one-line function. Functions initialized with lambda can often called anonymous functions


In [3]:
# Example lambda keyword

product_func = lambda x, y: x*y

print(product_func(10, 20))
print(product_func(10, 2))


200
20

In [4]:
concat = lambda x, y: [x, y]

print(concat([1,2,3], 4))


[[1, 2, 3], 4]

Functions as Objects

Functions are first-class objects in Python, meaning they have attributes and can be referenced and assigned to variables.


In [6]:
def square(x):
    """
    This returns the square of the requested number `x`
    """
    return x**2

print(square(10))


100

In [7]:
print(square(100))


10000

In [8]:
# Assignation to another variable
mySquare = square
print(mySquare(100))
print(square)
print(mySquare)
print(id(square))
print(id(mySquare))


10000
<function square at 0x00000219C2D6B268>
<function square at 0x00000219C2D6B268>
2309666288232
2309666288232

In [13]:
# attributes present
print("*"*30)
print(dir(square))
print("*"*30)
print(mySquare.__name__)
print("*"*30)
print(square.__code__)
print("*"*30)
print(square.__doc__)


******************************
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'd']
******************************
square
******************************
<code object square at 0x00000219C2D5AE40, file "<ipython-input-6-37fb4737804d>", line 1>
******************************

    This returns the square of the requested number `x`
    

Adding attributes to a function


In [12]:
square.d = 10
print(dir(square))


['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'd']

higher-order function

Python also supports higher-order functions, meaning that functions can accept other functions as arguments and return functions to the caller.


In [3]:
print(square(square(square(2))))


256

In [12]:
product_func = lambda x, y: x*y

sum_func = lambda F, m: lambda x, y: F(x, y)+m

print(sum_func(product_func, 5)(2, 4))


13

In [13]:
print(sum_func)


<function <lambda> at 0x7f1b2c089cf8>

In [16]:
print(sum_func(product_func, 5))


<function <lambda> at 0x7f1b2c089f50>

In [15]:
print(sum_func(product_func, 5)(3, 5))


20

13=2*4+5 F -> product_func m => 5 x -> 2 y -> 4 2*4+5 = 8+5 = 13

In the above example higher-order function that takes two inputs- A function F(x) and a multiplier m.

Nested Functions

In Python, Function(s) can also be defined within the scope of another function. If this type of function definition is used the inner function is only in scope inside the outer function, so it is most often useful when the inner function is being returned (moving it to the outer scope) or when it is being passed into another function.

Notice that in the below example, a new instance of the function inner() is created on each call to outer(). That is because it is defined during the execution of outer(). The creation of the second instance has no impact on the first.


In [23]:
def outer(a):
    """
    Outer function 
    """
    y = 0
    
    def inner(x):
        """
        inner function
        """
        y = x*x*a 
        return(y)
    print(a)
    
    return inner

my_out = outer

In [24]:
my_out(102)


102
Out[24]:
<function __main__.outer.<locals>.inner>

In [25]:
o = outer(10)
b = outer(20)
print("*"*20)
print(b)
print(o)
print("*"*20)
print(o(10))
print(b(10))


10
20
********************
<function outer.<locals>.inner at 0x00000219C2D82158>
<function outer.<locals>.inner at 0x00000219C2D6B510>
********************
1000
2000

In [21]:
def outer():
    """
    Outer function 
    """
    if 'a' in locals():
        a +=10
    else:
        print("~"),
        a = 20
    def inner(x):
        """
        inner function
        """
        return(x*x*a)
    print(a)
    return inner

# oo = outer
# print(oo.__doc__)
o = outer()
print("*"*20)
print(o)
print(o(10))
print(o.__doc__)


~ 20
********************
<function inner at 0x7f1b2c040758>
2000

        inner function
        

In [22]:
b = outer()
print(b)
print(b(30))
print(b.__doc__)


~ 20
<function inner at 0x7f1b2c0405f0>
18000

        inner function
        

In [9]:
x = 0

def outer():
    x = 1
    def inner():
        x = 2
        print("inner:", x)

    inner()
    print("outer:", x)

outer()
print("global:", x)


inner: 2
outer: 1
global: 0

Problem with local and global

lets take the above example, we have two functions, outer & inner. We also have x variable which is present as global and also present in both the functions.

If we want to access x of outer function from inner function than global keyword not help. Fortunately, Python provides a keyword nonlocal which allows inner functions to access variables to outer functions as shown in below example.


In [14]:
x = 0
def outer():
    x = 1
    def inner():
        nonlocal x
        x = 2
        print("inner:",x, "id:", id(x))

    inner()
    print("outer:",x, "id:", id(x))

outer()
print("global:",x, "id:", id(x))


inner: 2 id: 140130654173632
outer: 2 id: 140130654173632
global: 0 id: 140130654173568

In [6]:
def outer(a):
    """
    Outer function 
    """
    y = 1
    def inner(x):
        """
        inner function
        """
        nonlocal y
        print(y)
        y = x*x*a 
        return("y =" + str(y))
    print(a)
    return inner

o = outer(10)
b = outer(20)
print("*"*20)
print(o)
print(o(10))
print("*"*20)
print(b)
print(b(10))


10
20
********************
<function outer.<locals>.inner at 0x7f72adddabf8>
1
y =1000
********************
<function outer.<locals>.inner at 0x7f72a81397b8>
1
y =2000

Inner / Nested Functions - When to use

Encapsulation

You use inner functions to protect them from anything happening outside of the function, meaning that they are hidden from the global scope.


In [3]:
# Encapsulation

def increment(current):
    def inner_increment(x):  # hidden from outer code
        return x + 1
    next_number = inner_increment(current)
    return [current, next_number]

print(increment(10))


[10, 11]

NOTE: We can not access directly the inner function as shown below


In [4]:
try:
    increment.inner_increment(109)
except Exception  as e:
    print(e)


'function' object has no attribute 'inner_increment'

In [11]:
### NOT WORKING

def update(str_val):
    def updating(ori_str, key, value):
        token = "$"
        if key in ori_str:
            ori_str = ori_str.replace(token+key, value)
        return ori_str
    
    keyval = [{"test1": "val_test", "t1" : "val_1"}, {"test2": "val_test2", "t2" : "val_2"}]

    keyval1 = [{"test1": "val_test", "t1" : "val_1"}, {"test2": "val_test2", "t2" : "val_2"}]
    ori_str = "This is a $test1 and $test2, $t1 and $t2"
    
#     for k in keyval:
#         for key, value in k.items():
#             ori_str = updateing(ori_str, key, value)
    
    sdd = [ key, value [for key, value in k] for(k in keyval) ]
    
    print(ori_str)
    
update("D")


  File "<ipython-input-11-4c5edce7660a>", line 17
    sdd = [ key, value [for key, value in k] for(k in keyval) ]
                          ^
SyntaxError: invalid syntax

In [ ]:


In [18]:
ld = [{'a': 10, 'b': 20}, {'p': 10, 'u': 100}]
[kv for d in ld for kv in d.items()]


Out[18]:
[('a', 10), ('b', 20), ('p', 10), ('u', 100)]

In [10]:
ori_str = "This is a $test;1 and $test2, $t1 and $t2"
print(ori_str.replace("test1", "TEST1"))
print(ori_str)


This is a $TEST1 and $test2, $t1 and $t2
This is a $test1 and $test2, $t1 and $t2

Following DRY (Don't Repeat Yourself)

This type can be used if you have a section of code base in function is repeated in numerous places. For example, you might write a function which processes a file, and you want to accept either an open file object or a file name:


In [51]:
# Keepin’ it DRY

def process(file_name):
    def do_stuff(file_process):
        for line in file_process:
            print(line)

    if isinstance(file_name, str):
        with open(file_name, 'r') as f:
            do_stuff(f)
    else:
        do_stuff(file_name)
        
process(["test", "test3", "t33"])do_stuff(file_name)
        
process("test.txt")


test
test3
t33
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
<ipython-input-51-6cab43fac967> in <module>()
     14 process(["test", "test3", "t33"])
     15 
---> 16 process("test.txt")

<ipython-input-51-6cab43fac967> in process(file_name)
      7 
      8     if isinstance(file_name, str):
----> 9         with open(file_name, 'r') as f:
     10             do_stuff(f)
     11     else:

FileNotFoundError: [Errno 2] No such file or directory: 'test.txt'

or have similar logic which can be replaced by a function, such as mathematical functions, or code base which can be clubed by using some parameters.


In [52]:
def square(n):
    return n**2

def cube(n):
    return n**3

print(square(2))


4

In [37]:
def sqr(a, b):
    return a**b

??? why code


In [24]:
def test():
    print("TESTTESTTEST")
    def yes(name):
        print("Ja, ", name)
        return True
    return yes

d = test()
print("XSSSS")
print(d("Venky"))


TESTTESTTEST
XSSSS
Ja,  Venky
True

In [38]:
def power(exp):
    def subfunc(a):
        return a**exp
    return subfunc

square = power(2)
hexa = power(6)

print(square)
print(hexa)

print(square(5)) # 5**2
print()
print(hexa(3)) # 3**6
print(power(6)(3))
# subfunc(3) where exp = 6
# SQuare 

# exp -> 2 
# Square(5) 
# a -> 5 
# 5**2
# 25


<function power.<locals>.subfunc at 0x00000219C2D6BB70>
<function power.<locals>.subfunc at 0x00000219C2D6BEA0>
25

729
729

In [ ]:
Power(6)(3, x)

In [48]:
def a1(m):
    x = m * 2
    def b(v, t=None):
        if t:
            print(x, m, t)
            return v + t
        else:
            print(x, m, v)
            return v + x
    return b
n = a1(2)
print(n(3))
print(n(3, 10))


4 2 3
7
4 2 10
13

In [52]:
def f1(a):
    def f2(b):
        return f2
        def f3(c):
            return f3
            def f4(d):
                return f4
                def f5(e):
                    return f5
print (f1(1)(2)(3)(4)(5))


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-52-8b5968dce991> in <module>()
      8                 def f5(e):
      9                     return f5
---> 10 print (f1(1)(2)(3)(4)(5))

TypeError: 'NoneType' object is not callable

In [55]:
def f1(a):
    def f2(b):
        def f3(c):
            def f4(d):
                def f5(e):
                    print(e)
                return f5
            return f4
        return f3
    return f2
        
f1(1)(2)(3)(4)(5)


5

Closures & Factory Functions 1

They are techniques for implementing lexically scoped name binding with first-class functions. It is a record, storing a function together with an environment. a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.

A closure—unlike a plain function—allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.


In [12]:
def f(x):
    def g(y):
        return x + y
    return g

def h(x):
    return lambda y: x + y

a = f(1)
b = h(1)
print(a, b)
print(a(5), b(5))
print(f(1)(5), h(1)(5))


<function f.<locals>.g at 0x000001ECFA3A3268> <function h.<locals>.<lambda> at 0x000001ECFA3A3598>
6 6
6 6

both a and b are closures—or rather, variables with a closure as value—in both cases produced by returning a nested function with a free variable from an enclosing function, so that the free variable binds to the parameter x of the enclosing function. However, in the first case the nested function has a name, g, while in the second case the nested function is anonymous. The closures need not be assigned to a variable, and can be used directly, as in the last lines—the original name (if any) used in defining them is irrelevant. This usage may be deemed an "anonymous closure".

1: Copied from : "https://en.wikipedia.org/wiki/Closure_(computer_programming)"


In [25]:
def make_adder(x):
    def add(y):
        return x + y
    return add

plus10 = make_adder(10)
print(plus10(12))  # make_adder(10).add(12)
print(make_adder(10)(12))


22
22

Closures can avoid the use of global values and provides some form of data hiding. It can also provide an object oriented solution to the problem.

When there are few methods (one method in most cases) to be implemented in a class, closures can provide an alternate and more elegant solutions. But when the number of attributes and methods get larger, better implement a class.

In functional programming, functions can be treated as objects. That is, they can assigned to a variable, can be passed as arguments or even returned from other functions.


In [2]:
a = 10
def test_function():
    pass
print(id(a), dir(a))
print(id(test_function), dir(test_function))


1798467328 ['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
2309665771728 ['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

The lambda

The simplest way to initialize a pure function in python is by using lambda keyword, which helps in defining the one-line function. Functions initialized with lambda can often called anonymous functions


In [3]:
# Example lambda keyword

product_func = lambda x, y: x*y

print(product_func(10, 20))
print(product_func(10, 2))


200
20

In [4]:
concat = lambda x, y: [x, y]

print(concat([1,2,3], 4))


[[1, 2, 3], 4]

Functions as Objects

Functions are first-class objects in Python, meaning they have attributes and can be referenced and assigned to variables.


In [6]:
def square(x):
    """
    This returns the square of the requested number `x`
    """
    return x**2

print(square(10))


100

In [7]:
print(square(100))


10000

In [8]:
# Assignation to another variable
mySquare = square
print(mySquare(100))
print(square)
print(mySquare)
print(id(square))
print(id(mySquare))


10000
<function square at 0x00000219C2D6B268>
<function square at 0x00000219C2D6B268>
2309666288232
2309666288232

In [13]:
# attributes present
print("*"*30)
print(dir(square))
print("*"*30)
print(mySquare.__name__)
print("*"*30)
print(square.__code__)
print("*"*30)
print(square.__doc__)


******************************
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'd']
******************************
square
******************************
<code object square at 0x00000219C2D5AE40, file "<ipython-input-6-37fb4737804d>", line 1>
******************************

    This returns the square of the requested number `x`
    

Adding attributes to a function


In [12]:
square.d = 10
print(dir(square))


['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'd']

higher-order function

Python also supports higher-order functions, meaning that functions can accept other functions as arguments and return functions to the caller.


In [3]:
print(square(square(square(2))))


256

In [12]:
product_func = lambda x, y: x*y

sum_func = lambda F, m: lambda x, y: F(x, y)+m

print(sum_func(product_func, 5)(2, 4))


13

In [13]:
print(sum_func)


<function <lambda> at 0x7f1b2c089cf8>

In [16]:
print(sum_func(product_func, 5))


<function <lambda> at 0x7f1b2c089f50>

In [15]:
print(sum_func(product_func, 5)(3, 5))


20

13=2*4+5 F -> product_func m => 5 x -> 2 y -> 4 2*4+5 = 8+5 = 13

In the above example higher-order function that takes two inputs- A function F(x) and a multiplier m.

Nested Functions

In Python, Function(s) can also be defined within the scope of another function. If this type of function definition is used the inner function is only in scope inside the outer function, so it is most often useful when the inner function is being returned (moving it to the outer scope) or when it is being passed into another function.

Notice that in the below example, a new instance of the function inner() is created on each call to outer(). That is because it is defined during the execution of outer(). The creation of the second instance has no impact on the first.


In [23]:
def outer(a):
    """
    Outer function 
    """
    y = 0
    
    def inner(x):
        """
        inner function
        """
        y = x*x*a 
        return(y)
    print(a)
    
    return inner

my_out = outer

In [24]:
my_out(102)


102
Out[24]:
<function __main__.outer.<locals>.inner>

In [25]:
o = outer(10)
b = outer(20)
print("*"*20)
print(b)
print(o)
print("*"*20)
print(o(10))
print(b(10))


10
20
********************
<function outer.<locals>.inner at 0x00000219C2D82158>
<function outer.<locals>.inner at 0x00000219C2D6B510>
********************
1000
2000

In [21]:
def outer():
    """
    Outer function 
    """
    if 'a' in locals():
        a +=10
    else:
        print("~"),
        a = 20
    def inner(x):
        """
        inner function
        """
        return(x*x*a)
    print(a)
    return inner

# oo = outer
# print(oo.__doc__)
o = outer()
print("*"*20)
print(o)
print(o(10))
print(o.__doc__)


~ 20
********************
<function inner at 0x7f1b2c040758>
2000

        inner function
        

In [22]:
b = outer()
print(b)
print(b(30))
print(b.__doc__)


~ 20
<function inner at 0x7f1b2c0405f0>
18000

        inner function
        

In [9]:
x = 0

def outer():
    x = 1
    def inner():
        x = 2
        print("inner:", x)

    inner()
    print("outer:", x)

outer()
print("global:", x)


inner: 2
outer: 1
global: 0

Problem with local and global

lets take the above example, we have two functions, outer & inner. We also have x variable which is present as global and also present in both the functions.

If we want to access x of outer function from inner function than global keyword not help. Fortunately, Python provides a keyword nonlocal which allows inner functions to access variables to outer functions as shown in below example.


In [14]:
x = 0
def outer():
    x = 1
    def inner():
        nonlocal x
        x = 2
        print("inner:",x, "id:", id(x))

    inner()
    print("outer:",x, "id:", id(x))

outer()
print("global:",x, "id:", id(x))


inner: 2 id: 140130654173632
outer: 2 id: 140130654173632
global: 0 id: 140130654173568

In [6]:
def outer(a):
    """
    Outer function 
    """
    y = 1
    def inner(x):
        """
        inner function
        """
        nonlocal y
        print(y)
        y = x*x*a 
        return("y =" + str(y))
    print(a)
    return inner

o = outer(10)
b = outer(20)
print("*"*20)
print(o)
print(o(10))
print("*"*20)
print(b)
print(b(10))


10
20
********************
<function outer.<locals>.inner at 0x7f72adddabf8>
1
y =1000
********************
<function outer.<locals>.inner at 0x7f72a81397b8>
1
y =2000

Inner / Nested Functions - When to use

Encapsulation

You use inner functions to protect them from anything happening outside of the function, meaning that they are hidden from the global scope.


In [3]:
# Encapsulation

def increment(current):
    def inner_increment(x):  # hidden from outer code
        return x + 1
    next_number = inner_increment(current)
    return [current, next_number]

print(increment(10))


[10, 11]

NOTE: We can not access directly the inner function as shown below


In [4]:
try:
    increment.inner_increment(109)
except Exception  as e:
    print(e)


'function' object has no attribute 'inner_increment'

In [11]:
### NOT WORKING

def update(str_val):
    def updating(ori_str, key, value):
        token = "$"
        if key in ori_str:
            ori_str = ori_str.replace(token+key, value)
        return ori_str
    
    keyval = [{"test1": "val_test", "t1" : "val_1"}, {"test2": "val_test2", "t2" : "val_2"}]

    keyval1 = [{"test1": "val_test", "t1" : "val_1"}, {"test2": "val_test2", "t2" : "val_2"}]
    ori_str = "This is a $test1 and $test2, $t1 and $t2"
    
#     for k in keyval:
#         for key, value in k.items():
#             ori_str = updateing(ori_str, key, value)
    
    sdd = [ key, value [for key, value in k] for(k in keyval) ]
    
    print(ori_str)
    
update("D")


  File "<ipython-input-11-4c5edce7660a>", line 17
    sdd = [ key, value [for key, value in k] for(k in keyval) ]
                          ^
SyntaxError: invalid syntax

In [ ]:


In [18]:
ld = [{'a': 10, 'b': 20}, {'p': 10, 'u': 100}]
[kv for d in ld for kv in d.items()]


Out[18]:
[('a', 10), ('b', 20), ('p', 10), ('u', 100)]

In [10]:
ori_str = "This is a $test;1 and $test2, $t1 and $t2"
print(ori_str.replace("test1", "TEST1"))
print(ori_str)


This is a $TEST1 and $test2, $t1 and $t2
This is a $test1 and $test2, $t1 and $t2

Following DRY (Don't Repeat Yourself)

This type can be used if you have a section of code base in function is repeated in numerous places. For example, you might write a function which processes a file, and you want to accept either an open file object or a file name:


In [52]:
# Keepin’ it DRY

def process(file_name):
    def do_stuff(file_process):
        for line in file_process:
            print(line)

    if isinstance(file_name, str):
        with open(file_name, 'r') as f:
            do_stuff(f)
    else:
        do_stuff(file_name)
        
process(["test", "test3", "t33"])
# process("test.txt")


test
test3
t33

or have similar logic which can be replaced by a function, such as mathematical functions, or code base which can be clubed by using some parameters.


In [52]:
def square(n):
    return n**2

def cube(n):
    return n**3

print(square(2))


4

In [37]:
def sqr(a, b):
    return a**b

??? why code


In [24]:
def test():
    print("TESTTESTTEST")
    def yes(name):
        print("Ja, ", name)
        return True
    return yes

d = test()
print("XSSSS")
print(d("Venky"))


TESTTESTTEST
XSSSS
Ja,  Venky
True

In [38]:
def power(exp):
    def subfunc(a):
        return a**exp
    return subfunc

square = power(2)
hexa = power(6)

print(square)
print(hexa)

print(square(5)) # 5**2
print()
print(hexa(3)) # 3**6
print(power(6)(3))
# subfunc(3) where exp = 6
# SQuare 

# exp -> 2 
# Square(5) 
# a -> 5 
# 5**2
# 25


<function power.<locals>.subfunc at 0x00000219C2D6BB70>
<function power.<locals>.subfunc at 0x00000219C2D6BEA0>
25

729
729

In [ ]:
Power(6)(3, x)

In [48]:
def a1(m):
    x = m * 2
    def b(v, t=None):
        if t:
            print(x, m, t)
            return v + t
        else:
            print(x, m, v)
            return v + x
    return b
n = a1(2)
print(n(3))
print(n(3, 10))


4 2 3
7
4 2 10
13

In [52]:
def f1(a):
    def f2(b):
        return f2
        def f3(c):
            return f3
            def f4(d):
                return f4
                def f5(e):
                    return f5
print (f1(1)(2)(3)(4)(5))


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-52-8b5968dce991> in <module>()
      8                 def f5(e):
      9                     return f5
---> 10 print (f1(1)(2)(3)(4)(5))

TypeError: 'NoneType' object is not callable

In [55]:
def f1(a):
    def f2(b):
        def f3(c):
            def f4(d):
                def f5(e):
                    print(e)
                return f5
            return f4
        return f3
    return f2
        
f1(1)(2)(3)(4)(5)


5

Closures & Factory Functions 1

They are techniques for implementing lexically scoped name binding with first-class functions. It is a record, storing a function together with an environment. a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.

A closure—unlike a plain function—allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.


In [12]:
def f(x):
    def g(y):
        return x + y
    return g

def h(x):
    return lambda y: x + y

a = f(1)
b = h(1)
print(a, b)
print(a(5), b(5))
print(f(1)(5), h(1)(5))


<function f.<locals>.g at 0x000001ECFA3A3268> <function h.<locals>.<lambda> at 0x000001ECFA3A3598>
6 6
6 6

both a and b are closures—or rather, variables with a closure as value—in both cases produced by returning a nested function with a free variable from an enclosing function, so that the free variable binds to the parameter x of the enclosing function. However, in the first case the nested function has a name, g, while in the second case the nested function is anonymous. The closures need not be assigned to a variable, and can be used directly, as in the last lines—the original name (if any) used in defining them is irrelevant. This usage may be deemed an "anonymous closure".

1: Copied from : "https://en.wikipedia.org/wiki/Closure_(computer_programming)"


In [25]:
def make_adder(x):
    def add(y):
        return x + y
    return add

plus10 = make_adder(10)
print(plus10(12))  # make_adder(10).add(12)
print(make_adder(10)(12))


22
22

Closures can avoid the use of global values and provides some form of data hiding. It can also provide an object oriented solution to the problem.

When there are few methods (one method in most cases) to be implemented in a class, closures can provide an alternate and more elegant solutions. But when the number of attributes and methods get larger, better implement a class.


In [ ]: